<?php defined('SYSPATH') OR die('No direct access allowed.');

/**
 * Specifies that another property is required to be present.  For a date with the same db field, the two 
 * are appended together.  So for example, a date field and a time field where the time field requires the date.
 *
 */
class Requires extends Annotation {}

class Required extends Annotation {}

/**
 * This annotation indicates what data type a class member is:
 * string
 * array
 * enum
 * int
 * float
 * date - the date as a unix timestamp
 * set
 */
class Type extends Annotation {}

/**
 * I cant think of a better name for this one.  This tells the system what kind of access is allowed.
 * Create: can create an object or can specify field value on creation
 * Read: can read/view object or field value
 * Update: can update object or field value
 * Delete: can dekete object (not usable on a field, of course)
 *
 * Specified as a string: "CRUD" or "CR", etc.
 * Examples:  an ID field might only be specified on creation but cannot be updated.
 * 
 */
class Access extends Annotation {}

class Bounded extends Annotation {}

/**
 * Defines the allowed values for enum or set field types.
 *
 */
class Choices extends Annotation {
    public $keys;
    public $values;
}
class Immutable extends Annotation {}

/**
 * Determines display order
 */
class MemberOrder extends Annotation {
    public $sequence;
}

/**
 * Define an input validation string as a regular expression
 *
 */
class RegEx extends Annotation {
    public $validation;
}

/**
 * Indicates that the property is optional and does not have to be specified
 *
 */
class Optional extends Annotation {}

/**
 * This annotation when applied to a class indicates that this class members may represent fields in a table.
 * The default table can be specified on the class annotation
 * 
 * When applied to a member it indicates which table (if different than the default) and which field it represents.
 * An optional format allows for a transformation into the format defined in the db:
 * 
 * Type(date) : format using same format string as php's date() function 
 *
 */
class DbTable extends Annotation {
    public $table;
    public $field;
    public $format;
}

/**
* Indicates field is the primary database key.  If a primary key annotation does not exist, then the CRUD methods wont work.
*/ 
class PrimaryKey extends Annotation {}

/**
 * How to format the property when displayed.  The formatting string depends on the type:
 * 
 * Type(date) : format using same format string as php's date() function
 * Type(int) : number_format 
 * Type(set or enum) : name = display name, name = etc...
 */
class TypeFormat extends Annotation {}

/**
 * This annotation simply defines grouping with any other member with the same group name.  For JSON, this places
 * the members inside a block with the group name (ie, group : {member, member2, etc}).  In an HTML FORM it might
 * put the form entry fields inside of a fieldset tag.
 *
 */
class Group extends Annotation {}

/**
 * Do not display this field in GUI
 */ 
class Hidden extends Annotation {}

/**
 * Import the methods of another class (a 'trait') into this class.  
 * 
 * UseTrait(classname)
 *
 * @Target("class")
 */
class TraitUse extends Annotation {}

/**
 * Rename method in trait
 * 
 * TraitMethod(from=method, as=newmethod)
 * 
 * @Target("class")
 */
class TraitMethod extends Annotation {}

/**
 * Naked Model class.  The concept is simple:
 * 
 * 1. Business logic should be encapsulated in the domain object
 * 2. The interface(s) should be a representation of the domain object
 * 
 * It follows, then:
 * 3. The GUI or API should be generated from the definition of the object
 *    Similarly, object persistance (database store) could be largely generated from the definition
 * 
 * We may not get all the way there, but this is a start.
 * 
 * A subclass of Naked_Model may define its properties and then annotate them, adding meta data that cannot be 
 * easily represented in PHP code.  For example, the datatype; the table and field it represents; what values are
 * allowed; grouping; etc.
 * 
 * From this information, the class can generate SQL to Read and Update the table(s), Convert to and from JSON data
 * packets, perform security checks on the data, suppress data based on authorization level, etc.
 */
abstract class Naked_Model extends Model {
    protected $mAnnotations;
    protected $mProperties;
    protected $mPropertyAnnotations;
    protected $mMethodAnnotations;
    protected $mTraits;
    
    public function __construct($id = NULL)
    {
        // load database library into $this->db 
        parent::__construct($id);

        //** contemplate thyself
        $class = new ReflectionAnnotatedClass(get_class($this));
        $this->mAnnotations = $class;
    
        $this->mProperties = array();
        
        /*
         * lets get the properties sorted by class, higher inherited properties on top.
         */
        foreach($class->getProperties() as $property) {
            $properties[$property->getDeclaringClass()->getName()][] = $property;
        }
        $properties = array_reverse($properties, true);
        $sorted_properties = array();
        foreach($properties as $property) {
            $sorted_properties = array_merge($sorted_properties, $property);
        }
        //print_r($sorted_properties);
        
        /*
        * Unset all our public properties so that __set can take care of them, stuffing them instead into $mProperties array,
        * also stuff all the annotations for our properties into the mPropertyAnnotations array.
        * also remember what class each property was defined in
        */
        foreach($sorted_properties as $property) {
          if ($property->isPublic()) { 
            $pname = $property->getName();
            $this->mProperties[$pname] = $this->$pname;
            $this->mPropertyAnnotations[$pname] = $property;
            unset($this->$pname);
          }
        }
        
        /**
         * Now go through our methods and stuff their annotations away
         */
        foreach($class->getMethods() as $method) {
          if ($method->isPublic()) {
            $mname = $method->getName();
            $this->mMethodAnnotations[$mname] = $method;
          }
        }
    }
 
    public function __set($key, $val) {
        $this->mProperties[$key] = $val;
    }
    
    public function __get($key) {
        if (array_key_exists($key, $this->mProperties))
          return $this->mProperties[$key];
        else return $this->$key;
    }
  
    /**
     * Get an array of all our properties (includes all properties from parent classes)
     * This will include all inherited properties up to the class where this is called.
     *
     * @return array
     */
    public function GetProperties() {
        $properties = array();
        
        $class = new ReflectionAnnotatedClass(get_class($this));
        $this->mAnnotations = $class;

        /*
         * lets get the properties sorted by class, higher inherited properties on top.
         */
        foreach($class->getProperties() as $property) {
            $properties[$property->getDeclaringClass()->getName()][] = $property;
        }
        $properties = array_reverse($properties, true);
        $sorted_properties = array();
        foreach($properties as $property) {
            $sorted_properties = array_merge($sorted_properties, $property);
        }
        
        unset($properties);
        foreach($sorted_properties as $property) {
          if ($property->isPublic()) {
              $pname = $property->getName();
              $properties[$pname] = $pname;
          }
        }
                
        return $properties;
    }
    
    public function GetPropertyAnnotation($property, $ann) {
        if ($this->mPropertyAnnotations[$property]->hasAnnotation($ann)) return $this->mPropertyAnnotations[$property]->getAnnotation($ann);
        else return false;
    }
    
    public function GetAnnotation($ann) {
        if ($this->mAnnotations->hasAnnotation($ann)) return $this->mAnnotations->getAnnotation($ann);
        else return false;
    }

    public function GetPropertyByAnnotation($ann) {
        foreach($this->mPropertyAnnotations as $prop => $prop_ann) {
            if ($prop_ann->hasAnnotation($ann)) return $prop;
        }
    }
    
    public function __call($method, $args) {
        /**
        * catch all set* and get* calls and make them manipulate our properties (and use the proper class property, 
        * so that it can be caught by __set/__get or regular properties in a sub class)
        */
        if (substr($method,0,3) == 'set') {
            $property = substr($method,3);
            //** we could put in some validation here, based on docComments via reflection on the properties above
            $this->$property = $args[0];
            
            //** return a reference to ourselves for method chaining fun
            return $this;
        }
        else if (substr($method,0,3) == 'get') {
            $property = substr($method,3);
            return $this->$property;
        }
        
        /**
        * for any property, check if its an object and if so, call its choice method.  If not, check if it has a 
        * Choice annotation.  If not, fail.
        */
        else if (substr($method,-7) == 'Choices') {
            $property = substr($method,0,-7);
            //** we could put in some validation here, based on docComments via reflection on the properties above
            if (is_object($this->$property)) return call_user_func_array($this->$property->choices(), $args);
            //else if ($this->propertyAnnotations[$property]->hasAnnotation('Choices')) return $this->propertyAnnotations[$property]->getAnnotation('Choices')->value;
            else if (FALSE !== $choices = $this->GetPropertyAnnotation($property, 'Choices')) return explode(',',$choices->value);
        }

        else 
            throw new Exception("Undefined Method or Property: " . $method);
    }

    public function PropertyIsReadable($property_name) {
        $access = $this->GetPropertyAnnotation($property_name,'Access');
        if (FALSE !== $access) {
            $access = $access->value;
            if (strpos($access,'R') !== FALSE) $is_writable = true;
            else $is_writable = false;
        }
        else $is_writable = true;

      if ($is_writable AND FALSE == $this->GetPropertyAnnotation($property_name,'Hidden')) return true;
      else return false;
    }
    
    /**
     * Get the properly qualified full table and field names for referencing each property in the database
     *
     * @return array
     */
    public function GetDbFieldNames($data = null) {
        $ret = array();
 
        $db_table_default = $this->GetAnnotation('DbTable');
        $default_table = (FALSE === $db_table_default) ? "":$db_table_default->table;
        
        //** go through each property
        foreach($this->GetProperties() as $property_name => $val) {
            $e_property_name = substr($property_name, 1);

            if ($data == null) 
            {
                //** if we didnt provide data, then go by what's readable by default
                if (!$this->PropertyIsReadable($property_name)) continue;
            }
            else 
            {
                //** if there's a group, then look into the proper index to find the value
                $group = $this->GetPropertyAnnotation($property_name, 'Group');
                if (FALSE !== $group)
                {
                    $group_name = $group->value;
                    if (!isset($data[$group_name]) OR !isset($data[$group_name][$e_property_name])) continue;
                } else
                {
                    if (!isset($data[$e_property_name])) continue;
                }
            }
            
            //** if we provided a data (JSON), check if this property is in there, otherwise there's nothing to insert for this property
            //if (null == $data OR isset($data[$e_property_name]))
            //{
                //** if theres a DbTable annotation, then this value goes into a table field.  Otherwise, nothing
                $db_table = $this->GetPropertyAnnotation($property_name, 'DbTable');
                if (FALSE !== $db_table) {
                    $ret[] = (($db_table->table) ? $db_table->table:$default_table) . '.' . $db_table->field;
                }
            //}
        }
        return $ret;
        
    }
    
    /**
     * Validate the data against annotated fields.
     * 
     * Returns null if all validations pass, otherwise, return an array containing the data in the fields that
     * did not pass.
     *
     * @param JSON $data
     */
    public function ValidateData($validateData) {
        $ret = array();

        //** go through each property
        foreach($this->GetProperties() as $property_name => $val) {
            $e_property_name = substr($property_name, 1);

            if (!array_key_exists($e_property_name,$validateData)) continue;

            //** check against a regular expression?
            $regex = $this->GetPropertyAnnotation($property_name, 'RegEx');
            if (FALSE !== $regex) {
                if (preg_match($regex->value, $validateData[$e_property_name]) == 0) {
                    $ret[$e_property_name] = $validateData[$e_property_name];
                    continue;
                }
            }
            
            //** generic type checking
            $type = $this->GetPropertyAnnotation($property_name, 'Type');
            $data = $validateData[$e_property_name];
            if (FALSE !== $type) {
                switch ($type->value) {
                    case 'int':
                        if (!(($data !== true) && ((string)(int) $data) === ((string) $data))) $ret[$e_property_name] = $data;
                        break;
                    case 'float':
                        if (!(($data !== true) && ((string)(float) $data) === ((string) $data))) $ret[$e_property_name] = $data;
                        break;
                    case 'enum':
                    case 'set':
                        break;
                }
            }
            
            
        }

        if (sizeof($ret) > 0) return $ret;
        else return null;
    }
    
    /**
     * Get the property values out of a data object/array (typicaly JSON data) and put them into a flat array
     * to be fed into a query in the same order as the field names from GetDbFieldNames.
     * 
     * @param json $data
     * @param int $memberPrefix 
     * @return array
     */
    public function GetDataValues($data, $memberPrefix = 1) {
        $ret = array();
        $properties = $this->GetProperties();

        //** go through each property
        foreach($properties as $property_name => $val) {
            $e_property_name = substr($property_name,$memberPrefix);
            
            //** if there's a group, then look into the proper index to find the value
            $group = $this->GetPropertyAnnotation($property_name, 'Group');
            if (FALSE !== $group)
            {
                $group_name = $group->value;
                if (!isset($data[$group_name]) OR !isset($data[$group_name][$e_property_name])) continue;
                $value = $data[$group_name][$e_property_name];
            } else
            {
                if (!isset($data[$e_property_name])) continue;
                $value = $data[$e_property_name];
            }

            /*$requires = $this->GetPropertyAnnotation($property_name, 'Requires');
            if (FALSE !== $type) {
                $properties[$requires->value]
            }*/
            
            //** normalize the data value.  For now, just handle the dates.
            $type = $this->GetPropertyAnnotation($property_name, 'Type');
            if (FALSE !== $type) {
                if ("date" == $type->value) $value = strtotime($value);
            }
            
            //** format it for the database
            $db_table = $this->GetPropertyAnnotation($property_name, 'DbTable');
            if (FALSE !== $db_table) {
                if ($db_table->format) {
                    if ("date" == $type->value)
                      $value = date($db_table->format, $value);
                }

                $ret[$e_property_name] = $value;
            }

        }
        return $ret;
    }
    
    /**
     * Accepts a db result row and formats it into a JSON data packet
     *
     * @param db result $result
     * @return json 
     */
    public function SetDataValues($result, $memberPrefix = 1) {
        $return = array();
        
        $properties = $this->GetProperties();
        //** go through each property
        foreach($properties as $property_name => $val) {
            //** skip ones that are not readable
            if (!$this->PropertyIsReadable($property_name)) continue;
            
            $e_property_name = substr($property_name,$memberPrefix);
            $db_table = $this->GetPropertyAnnotation($property_name, 'DbTable');
            if (FALSE !== $db_table)
            {
                $db_field = $db_table->field;

                if (isset($result->$db_field))
                    $value = $result->$db_field;
                else
                    $value = null;
                    
                $group = $this->GetPropertyAnnotation($property_name, 'Group');
                if (FALSE !== $group)
                {
                    $return[$group->value][$e_property_name] = $value;
                }
                else
                {
                    $return[$e_property_name] = $value;
                }
            }
        }
        return $return;
    }

    public function Create($data) {
        $fields = $this->GetDbFieldNames($data);
        $values = $this->GetDataValues($data);        

        $db_table = $this->GetAnnotation('DbTable');
        if (FALSE !== $db_table)
        {            
            $result = $this->db->query("INSERT INTO {$db_table->table} SET " . implode(' = ?,',$fields) . " = ?", $values);      
            return true;
        }
        
        return false;
    }

    public function Read($id = null) {
        $db_table = $this->GetAnnotation('DbTable');
        if (FALSE !== $db_table AND $primary_key = $this->GetPropertyByAnnotation("PrimaryKey"))
        {   
            $primary_key_field = $this->GetPropertyAnnotation($primary_key, 'DbTable')->field;
            
            if ($id)
                $res = $this->db->query("SELECT * FROM {$db_table->table} WHERE " . $primary_key_field . " = ?", $id);
            else
                $res = $this->db->query("SELECT * FROM {$db_table->table}");

            if ($res->count() > 1) {
                $many = Array();
                
                foreach($res as $row) {
                    $many[] = $this->SetDataValues($row);
                }

                return $many;
            }
            else if ($res->count() == 0) {
                return false;
            }
            else {
                return $this->SetDataValues($res->current());
            }            
        }

        return false;
    }

    public function Update($id, $data) {
        $fields = $this->GetDbFieldNames($data);
        $values = $this->GetDataValues($data);

        $db_table = $this->GetAnnotation('DbTable');
        if (FALSE !== $db_table AND $primary_key = $this->GetPropertyByAnnotation("PrimaryKey"))
        {
            $primary_key_field = $this->GetPropertyAnnotation($primary_key, 'DbTable')->field;
            $values[] = $id;
            $this->db->query("UPDATE {$db_table->table} SET " . implode(' = ?,',$fields) . " = ? WHERE " . $primary_key_field . " = ?", $values);

            return true;
        }
    }
    
    public function Delete($id) {
        $db_table = $this->GetAnnotation('DbTable');
        if (FALSE !== $db_table)
        {
            $values[] = $id;
            $this->db->query("DELETE {$db_table->table} WHERE " . $this->GetPropertyByAnnotation("PrimaryKey"), $id);

            return true;
        }
    }
}
?>